using System;
using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;
using System.Configuration;


namespace BizElements.BusinessLayer
{
    /// <summary>
    /// Utility/facade class which wraps and extends common Ms Caching Application Block functionalities when caching per-user data.
    /// </summary>
    /// <remarks>Upgrades Ms Caching Application Block by introducing global item categories. Shares
    /// CacheManager instance and categories with <see cref="GlobalCache"/>.</remarks>
    public static class UserCache
    {
        #region Initialization and configuration.

        static int defaultAbsoluteExpirationInMinutes = 60;
        static ICacheManager cache;

        static UserCache()
        {
            cache = CacheFactory.GetCacheManager();
            int? cfgExpiresAfter = NumberParser.ParseInt(ConfigurationManager.AppSettings["BizElements.BusinessLayer.UserCache.DefaultAbsoluteExpirationInMinutes"]);
            if (cfgExpiresAfter.HasValue && cfgExpiresAfter.Value > 0)
                defaultAbsoluteExpirationInMinutes = cfgExpiresAfter.Value;
        }

        /// <summary>
        /// Gets or sets default absolute expiration in minutes.
        /// </summary>
        /// <value>Absolute expiration used by <see cref="SetData(IActor, string, object, string[])"/> method. Default is 60. Cannot be less than 1.</value>
        /// <exception cref="ArgumentException">Generated if a value less than 1 is set.</exception>
        /// <remarks>Default value may be set in the <b>appSettings</b> section of application's configuration file.
        /// <code>
        /// <add key="BizElements.BusinessLayer.UserCache.DefaultAbsoluteExpirationInMinutes" value="60"/>
        /// </code>
        /// </remarks>
        public static int DefaultAbsoluteExpirationInMinutes
        {
            get { return defaultAbsoluteExpirationInMinutes; }
            set
            {
                if (value < 1)
                    throw new ArgumentException("value", "Default absolute expiration may not be less than 1 minute.");

                defaultAbsoluteExpirationInMinutes = value;
            }
        }

        /// <summary>
        /// Indicates whether the cache is disabled in configuration file.
        /// </summary>
        /// <value><b>true</b> indicates that cache is disabled. Default is <b>false</b>.</value>
        /// <remarks>Value is set in the <b>appSettings</b> section of application's configuration file.
        /// <code>
        /// <add key="BizElements.BusinessLayer.UserCache.CachingDisabled" value="true"/>
        /// </code>
        /// </remarks>
        public static readonly bool CachingDisabled = NumberParser.IsTrue(ConfigurationManager.AppSettings["BizElements.BusinessLayer.UserCache.CachingDisabled"]);

        #endregion

        #region CacheManager wrapper. Category management is implemented in GlobalCache.

        private static string CreateKey(IActor actor, string key)
        {
            if (actor == null)
                throw new ArgumentNullException("actor", "Actor is required.");

            if (string.IsNullOrEmpty(key))
                throw new ArgumentException("key", "Null reference or empty string is not a valid key value.");

            return "UserCache_" + actor.Id.ToString() + "_" + key;
        }

        private static string GetCategory(IActor actor)
        {
            if (actor == null)
                throw new ArgumentNullException("actor", "Actor is required.");

            return "UserCache_" + actor.Id.ToString();
        }

        /// <summary>
        /// Returns the value associated with the given key.
        /// </summary>
        /// <param name="actor">Actor/user for which the data is cached.</param>
        /// <param name="key">Key of item to return from cache.</param>
        /// <returns>Value stored in cache or null if cache doesn't contain the specified key.</returns>
        public static object GetData(IActor actor, string key)
        {
            return cache.GetData(CreateKey(actor, key));
        }

        /// <summary>
        /// Adds new CacheItem to cache.
        /// </summary>
        /// <param name="actor">Actor/user for which the data is cached.</param>
        /// <param name="key">Identifier for this CacheItem. Null or empty string is not allowed.</param>
        /// <param name="value">Value to be stored in cache. May be null.</param>
        /// <param name="globalCategories">Optional list of global cache item categories to which the new item 
        /// belongs to. When a category is removed, all items it contains are removed, too.</param>
        /// <remarks><para>Adds new CacheItem to cache. If another item already exists with the same key, 
        /// that item is removed before the new item is added. If any failure occurs during this process, 
        /// the cache will not contain the item being added.</para>
        /// <para>Items added with this method expire absolutely after period defined by 
        /// <see cref="DefaultAbsoluteExpirationInMinutes"/> property.</para></remarks>
        public static void SetData(IActor actor, string key, object value, params string[] globalCategories)
        {
            SetData(actor, key, value, defaultAbsoluteExpirationInMinutes, false, globalCategories);
        }

        /// <summary>
        /// Adds new CacheItem to cache.
        /// </summary>
        /// <param name="actor">Actor/user for which the data is cached.</param>
        /// <param name="key">Identifier for this CacheItem. Null or empty string is not allowed.</param>
        /// <param name="value">Value to be stored in cache. May be null.</param>
        /// <param name="minutes">Time in minutes after which the cache item will expire. May not be less than 1 minute.</param>
        /// <param name="sliding">Indicates whether sliding or absolute time expiration schema is used for this item.</param>
        /// <param name="globalCategories">Optional list of global cache item categories to which the new item 
        /// belongs to. When a category is removed, all items it contains are removed, too.</param>
        /// <remarks><para>Adds new CacheItem to cache. If another item already exists with the same key, 
        /// that item is removed before the new item is added. If any failure occurs during this process, 
        /// the cache will not contain the item being added.</para></remarks>
        public static void SetData(IActor actor, string key, object value, int minutes, bool sliding, params string[] globalCategories)
        {
            if (CachingDisabled)
                return;

            if (minutes < 1)
                throw new ArgumentException("minutes", "Expiration period may not be less than 1 minute.");

            ICacheItemExpiration expiration;
            if (sliding)
                expiration = new SlidingTime(new TimeSpan(0, minutes, 0));
            else
                expiration = new AbsoluteTime(new TimeSpan(0, minutes, 0));

            if (ArrayUtil.IsNullOrEmpty(globalCategories))
                globalCategories = new string[] { GetCategory(actor) };
            else
                globalCategories = ArrayUtil.Union<string>(new string[] { GetCategory(actor) }, globalCategories);

            ICacheItemRefreshAction autoRemoveFromCategories = (globalCategories.Length > 0)
                ? new GlobalCache.RemoveScavengedItemFromGlobalCategories() : null;

            string itemCacheKey = CreateKey(actor, key);
            cache.Add(itemCacheKey, value, CacheItemPriority.Normal, autoRemoveFromCategories, expiration);
            GlobalCache.CategorizeItem(itemCacheKey, globalCategories);
        }

        /// <summary>
        /// Removes the given item from the cache.
        /// </summary>
        /// <param name="actor">Actor/user for which the data is cached.</param>
        /// <param name="key">Key of item to remove from cache.</param>
        /// <remarks><para>If no item exists with that key, this method does nothing.</para>
        /// <para>Item is also removed from all categories it any were specified.</para></remarks>
        public static void Remove(IActor actor, string key)
        {
            // RemoveScavengedItemFromGlobalCategories automatically handles the removal of categorized 
            // items. No need to manually call RemoveCacheItemFromAllCategories method.
            string itemCacheKey = CreateKey(actor, key);
            cache.Remove(itemCacheKey);
        }

        /// <summary>
        /// Returns true if key refers to a non-null item currently stored in cache.
        /// </summary>
        /// <param name="actor">Actor/user for which the data is cached.</param>
        /// <param name="key">Key of item to remove from cache.</param>
        /// <returns>True if item referenced by key is in the cache.</returns>
        public static bool Contains(IActor actor, string key)
        {
            object item = GetData(actor, key);
            return (item != null);
        }

        #endregion

        /// <summary>
        /// Remove all user items from cache.
        /// </summary>
        /// <param name="actor">Actor/user whise cached date is flushed.</param>
        public static void Flush(IActor actor)
        {
            string actorCategory = GetCategory(actor);
            GlobalCache.RemoveCategory(actorCategory);
        }

        /// <summary>
        /// Remove all user items from cache.
        /// </summary>
        /// <param name="actor">Actor/user whise cached date is flushed.</param>
        /// <param name="flushCacheInRemoteDomain">Indicates whether to flush cache in the remote domain, too.</param>
        public static void Flush(IActor actor, bool flushCacheInRemoteDomain)
        {
            string actorCategory = GetCategory(actor);
            GlobalCache.RemoveCategory(actorCategory);
            if (flushCacheInRemoteDomain && RpcHelper.MethodRemoter.ImplementsRemoting)
                RpcHelper.InvokeStaticMethod(typeof(GlobalCache), "RemoveCategory", actorCategory);
        }
    }
}
